/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.ldap;

import org.activiti.engine.ActivitiException;
import org.activiti.engine.ActivitiIllegalArgumentException;
import org.activiti.engine.identity.Group;
import org.activiti.engine.identity.Picture;
import org.activiti.engine.identity.User;
import org.activiti.engine.identity.UserQuery;
import org.activiti.engine.impl.Page;
import org.activiti.engine.impl.UserQueryImpl;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.persistence.AbstractManager;
import org.activiti.engine.impl.persistence.entity.IdentityInfoEntity;
import org.activiti.engine.impl.persistence.entity.UserEntity;
import org.activiti.engine.impl.persistence.entity.UserIdentityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Implementation of the {@link UserIdentityManager} interface specifically for LDAP.
 * <p>
 * Note that only a few methods are actually implemented, as many of the operations
 * (save, update, etc.) are done on the LDAP system directly.
 *
 * @author Joram Barrez
 */
public class LDAPUserManager extends AbstractManager implements UserIdentityManager {

    private static Logger logger = LoggerFactory.getLogger(LDAPUserManager.class);

    protected LDAPConfigurator ldapConfigurator;

    public LDAPUserManager(LDAPConfigurator ldapConfigurator) {
        this.ldapConfigurator = ldapConfigurator;
    }

    @Override
    public User createNewUser(String userId) {
        throw new ActivitiException("LDAP user manager doesn't support creating a new user");
    }


    @Override
    public void insertUser(User user) {
        throw new ActivitiException("LDAP user manager doesn't support inserting a new user");
    }


    @Override
    public void updateUser(User updatedUser) {
        throw new ActivitiException("LDAP user manager doesn't support updating a user");
    }

    @Override
    public boolean isNewUser(User user) {
        throw new ActivitiException("LDAP user manager doesn't support adding or updating a user");
    }


    @Override
    public UserEntity findUserById(final String userId) {
        LDAPTemplate ldapTemplate = new LDAPTemplate(ldapConfigurator);
        return ldapTemplate.execute(new LDAPCallBack<UserEntity>() {

            public UserEntity executeInContext(InitialDirContext initialDirContext) {
                try {

                    String searchExpression = ldapConfigurator.getLdapQueryBuilder().buildQueryByUserId(ldapConfigurator, userId);

                    String baseDn = ldapConfigurator.getUserBaseDn() != null ? ldapConfigurator.getUserBaseDn() : ldapConfigurator.getBaseDn();
                    NamingEnumeration<?> namingEnum = initialDirContext.search(baseDn, searchExpression, createSearchControls());
                    UserEntity user = new UserEntity();
                    while (namingEnum.hasMore()) { // Should be only one
                        SearchResult result = (SearchResult) namingEnum.next();
                        mapSearchResultToUser(result, user);
                    }
                    namingEnum.close();

                    return user;

                } catch (NamingException ne) {
                    logger.debug("Could not find user " + userId + " : " + ne.getMessage(), ne);
                    return null;
                }
            }

        });
    }


    @Override
    public void deleteUser(String userId) {
        throw new ActivitiException("LDAP user manager doesn't support deleting a user");
    }


    @Override
    public List<User> findUserByQueryCriteria(final UserQueryImpl query, final Page page) {

        if (query.getId() != null) {
            List<User> result = new ArrayList<User>();
            result.add(findUserById(query.getId()));
            return result;
        } else if (query.getFullNameLike() != null) {

            final String fullNameLike = query.getFullNameLike().replaceAll("%", "");

            LDAPTemplate ldapTemplate = new LDAPTemplate(ldapConfigurator);
            return ldapTemplate.execute(new LDAPCallBack<List<User>>() {

                public List<User> executeInContext(InitialDirContext initialDirContext) {
                    List<User> result = new ArrayList<User>();
                    try {
                        String searchExpression = ldapConfigurator.getLdapQueryBuilder().buildQueryByFullNameLike(ldapConfigurator, fullNameLike);
                        String baseDn = ldapConfigurator.getUserBaseDn() != null ? ldapConfigurator.getUserBaseDn() : ldapConfigurator.getBaseDn();
                        NamingEnumeration<?> namingEnum = initialDirContext.search(baseDn, searchExpression, createSearchControls());

                        while (namingEnum.hasMore()) {
                            SearchResult searchResult = (SearchResult) namingEnum.next();

                            UserEntity user = new UserEntity();
                            mapSearchResultToUser(searchResult, user);
                            result.add(user);

                        }
                        namingEnum.close();

                    } catch (NamingException ne) {
                        logger.debug("Could not execute LDAP query: " + ne.getMessage(), ne);
                        return null;
                    }
                    return result;
                }

            });

        } else {
            throw new ActivitiIllegalArgumentException("Query is currently not supported by LDAPUserManager.");
        }

    }

    protected void mapSearchResultToUser(SearchResult result, UserEntity user) throws NamingException {
        if (ldapConfigurator.getUserIdAttribute() != null) {
            user.setId(result.getAttributes().get(ldapConfigurator.getUserIdAttribute()).get().toString());
        }
        if (ldapConfigurator.getUserFirstNameAttribute() != null) {
            try {
                user.setFirstName(result.getAttributes().get(ldapConfigurator.getUserFirstNameAttribute()).get().toString());
            } catch (NullPointerException e) {
                user.setFirstName("");
            }
        }
        if (ldapConfigurator.getUserLastNameAttribute() != null) {
            try {
                user.setLastName(result.getAttributes().get(ldapConfigurator.getUserLastNameAttribute()).get().toString());
            } catch (NullPointerException e) {
                user.setLastName("");
            }
        }
        if (ldapConfigurator.getUserEmailAttribute() != null) {
            try {
                user.setEmail(result.getAttributes().get(ldapConfigurator.getUserEmailAttribute()).get().toString());
            } catch (NullPointerException e) {
                user.setEmail("");
            }
        }
    }

    @Override
    public long findUserCountByQueryCriteria(UserQueryImpl query) {
        return findUserByQueryCriteria(query, null).size(); // Is there a generic way to do counts in ldap?
    }


    @Override
    public List<Group> findGroupsByUser(String userId) {
        throw new ActivitiException("LDAP user manager doesn't support querying");
    }

    @Override
    public UserQuery createNewUserQuery() {
        return new UserQueryImpl(Context.getProcessEngineConfiguration().getCommandExecutor());
    }

    @Override
    public IdentityInfoEntity findUserInfoByUserIdAndKey(String userId, String key) {
        throw new ActivitiException("LDAP user manager doesn't support querying");
    }

    @Override
    public List<String> findUserInfoKeysByUserIdAndType(String userId, String type) {
        throw new ActivitiException("LDAP user manager doesn't support querying");
    }

    @Override
    public List<User> findPotentialStarterUsers(String proceDefId) {
        throw new ActivitiException("LDAP user manager doesn't support querying");
    }

    @Override
    public List<User> findUsersByNativeQuery(Map<String, Object> parameterMap, int firstResult, int maxResults) {
        throw new ActivitiException("LDAP user manager doesn't support querying");
    }

    @Override
    public long findUserCountByNativeQuery(Map<String, Object> parameterMap) {
        throw new ActivitiException("LDAP user manager doesn't support querying");
    }

    @Override
    public void setUserPicture(String userId, Picture picture) {
        throw new ActivitiException("LDAP user manager doesn't support user pictures");
    }

    @Override
    public Picture getUserPicture(String userId) {
        logger.debug("LDAP user manager doesn't support user pictures. Returning null");
        return null;
    }

    @Override
    public Boolean checkPassword(final String userId, final String password) {

        // Extra password check, see http://forums.activiti.org/comment/22312
        if (password == null || password.length() == 0) {
            throw new ActivitiException("Null or empty passwords are not allowed!");
        }

        try {
            LDAPTemplate ldapTemplate = new LDAPTemplate(ldapConfigurator);
            return ldapTemplate.execute(new LDAPCallBack<Boolean>() {

                public Boolean executeInContext(InitialDirContext initialDirContext) {

                    if (initialDirContext == null) {
                        return false;
                    }

                    // Do the actual search for the user
                    String userDn = null;
                    try {

                        String searchExpression = ldapConfigurator.getLdapQueryBuilder().buildQueryByUserId(ldapConfigurator, userId);
                        String baseDn = ldapConfigurator.getUserBaseDn() != null ? ldapConfigurator.getUserBaseDn() : ldapConfigurator.getBaseDn();
                        NamingEnumeration<?> namingEnum = initialDirContext.search(baseDn,
                                searchExpression, createSearchControls());

                        while (namingEnum.hasMore()) { // Should be only one
                            SearchResult result = (SearchResult) namingEnum.next();
                            userDn = result.getNameInNamespace();
                        }
                        namingEnum.close();

                    } catch (NamingException ne) {
                        logger.info("Could not authenticate user " + userId + " : " + ne.getMessage(), ne);
                        return false;
                    }

                    // Now we have the user DN, we can need to create a connection it
                    // ('bind' in ldap lingo)
                    // to check if the user is valid
                    if (userDn != null) {
                        InitialDirContext verificationContext = null;
                        try {
                            verificationContext = LDAPConnectionUtil.createDirectoryContext(ldapConfigurator, userDn, password);
                        } catch (ActivitiException e) {
                            // Do nothing, an exception will be thrown if the login fails
                        }

                        if (verificationContext != null) {
                            LDAPConnectionUtil.closeDirectoryContext(verificationContext);
                            return true;
                        }
                    }

                    return false;

                }
            });

        } catch (ActivitiException e) {
            logger.info("Could not authenticate user : " + e);
            return false;
        }
    }

    protected SearchControls createSearchControls() {
        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        searchControls.setTimeLimit(ldapConfigurator.getSearchTimeLimit());
        return searchControls;
    }

}